<?php
/*
 * Ext_Closures_Closure es una abstracción de codigo ejecutable. La idea es poder  
 * crear codigo 'tipo-criterio' en pocas lineas sin tener que crear una clase 
 * o una funcion aparte.
 * 
 * Features
 * 		- Uso del contexto del closure
 * 		- Curryfication del closure
 * 		- Compilacion a demanda 
 * --Contexto--
 * 		Varias veces se requiere que la ejecución de una función dependa 
 * 		del contexto de ejecución ademas de los parametros. 
 * 		
 * 		El paso de valores del contexto es un tanto sucio, pero es por restriccion del lenguaje. 
 * 		(Para hacerlo mas prolijo habria que precompilar).
 * 
 * 		$idRequest = $_GET["id"];
 * 		$closure = new Exts_Closures_Closure ("$fila", " return $fila->id == $idRequest");
 * 		$closure->useFromContext('idRequest', $idRequest);
 * 		-- Otras formas de asignacion de valores contextuales --
 * 			$closure->idRequest = $idRequest;
 * 			$closure->useContext (array("idRequest"=>$idRequest));
 * 
 * --Curryfication--
 * 		(Ver curryfication / Haskell Curry) 
 * 		El Closure es una abstraccion de funcion que puede ser invocada con menos 
 * 		argumentos que los que se usaron para definirla; Cuando se invoca con 
 * 		menos argumentos, la invocacion retorna un nuevo closure donde 
 * 		los m argumentos pasados seran pasados como m primeros parametros 
 * 		(de los n con que se definio) y el nuevo closure acepta n-m parametros 
 *  	
 *  	$closure = new Exts_Closures_Closure ("$a, $b, $c, $d", " return $a + $b + $c + $d; "); 
 *  		--Similar a: function fn ($a, $b, $c, $d) {return $a + $b + $c + $d;}
 *  	$nuevo = $closure->invoke(1,2);
 *  		--Similar a: function fn ($c, $d) {return 1 + 2 + $c + $d;} 
 *  	$nuevo->invoke(3,4);   fn ($a, $b, $c, $d)
 *  		-- Da por resultado 1+2+3+4 = 10
 *  	$closure->invoke(1,2,3,4);
 *  		-- Da por resultado 1+2+3+4 = 10
 *  

 */

class Exts_Closures_Closure /*implements Exts_Closures_Abstract */{
	protected $_variables;
	protected $_pushedValues;
	protected $_context = array();
	protected $_code;
	protected $_function;
	
	/*
	 * Constructor: Solo en la construccion se permite definir las variables y el codigo.
	 * 
	 * */
	
	public function __construct($variables, $code) {
		$this->_code = $code;
		if ($variables != ""){
			$this->_variables = '$_self,'.$variables;
		}else {
			$this->_variables = '$_self';
		}
		$this->_pushedValues = array($this);
		$this->_setAsDirty();
	}
	
	
	/*
	 *	Accesors de contexto. Estos metodos publicos son para asignar valores de contexto;
	 *	En caso de extensión y sobreescritura de metodos es necesario mantener la llamada 
	 *	a $this->_setAsDirty()! (Utilizado en la generación de codigo) 
	 * */
	public function __get($name) {
		
		if ($name[0] != "$") $name = "$".$name;
		return $this->_context[$name];
	}
	
	public function __set($name, $value) {
		$this->useFromContext('$'.$name, $value);
		return $this;
	}
	
	public function useFromContext ($key, $value){
		$this->_context[$key] = $value;
		$this->_setAsDirty();
		return $this;
	}
	public function useContext (array $keyValueArray){
		$this->_context = $keyValueArray;
		$this->_setAsDirty();
		return $this;
	}
	
	/*
	 * 	Las distintas formas de pushValue son para ir guardando argumentos 
	 * (para el soporte de Curryfication) 
	 **/
	
	protected function _pushValue ($value) {
		if ($this->_expectedArguments() <= 0){
			throw new Exts_Exception ("Error al pushear parametro!");
		}
		array_push($this->_pushedValues, $value);
		return $this;
	}
	protected function _usePushedValues(array $values){
		$this->_pushedValues = $values;
		return $this;
	}
	/*
	 *	Los argumentos que se esperan ante una llamada son los que 
	 *	se definieron en la construccion menos los pusheados.
	 **/

	protected function _expectedArguments() {
		
		return count(explode(",", $this->_variables)) - count($this->_pushedValues);
	}

	/*
	 *	Dirtyness, La funcion a ejecutar se genera cada vez que el closure este marcado como sucio.
	 *	Se marca como sucio cuando hay modificaciones en el valor de contexto.
	 **/
	protected function _setAsDirty() {
		$this->_function = NULL;
	}
	protected function _isDirty() {
		return $this->_function == NULL;
	}
	
	/*
	 * Como decia mas arriba, getFunction se fija si el closure esta sucio, si esta crea 
	 * la funcion nuevamente
	 * */
	protected function _getFunction () {
		if ($this->_isDirty()) {
			$this->_function = create_function ($this->_variables, $this->_makeCode());
		}
		return $this->_function;
	}
	
	/*
	 * El paso de datos de contexto es bastante sencillo, se agrega al principio 
	 * del cuerpo de la función variable = valor segun lo que este seteado en el array de contexto. 
	 * */

	protected function _getCode() {
		return $this->_code;
	}
	protected function _makeCode() {
		$code = "";
		foreach ($this->_context as $key => $value){
				
			if ($key[0] != '$'){
				$variable = "$";
			} else {
				$variable = "";
			}
			
			$variable .= $key;
			
			$code .=  $variable . '= $_self->__get(\''.$key.'\'); ';
		}
		
		$code .= $this->_getCode();
		return $code; 
	}
	
	
	
	/*
	 * _makeNewClosure tiene como objetivo crear un closure nuevo con los argumentos recibidos como fijos.
	 * */
	protected function _makeNewClosure(array $arguments){
		$newClosure = $this->_newInstance($this->_variables, $this->_getCode());
		$newClosure->useContext($this->_context);
		$newClosure->_usePushedValues($this->_pushedValues);
		foreach ($arguments as $argument){
			$newClosure->_pushValue($argument);
		}
		return $newClosure;
	}
	/*
	 * 	Las nuevas instancias han de ser de la misma clase.
	 * */
	protected function _newInstance ($variables, $code) {
		$class = get_class($this);
		return new $class($variables, $code);
	}
	
	protected function _generateArguments(array $arguments){
		return array_merge($this->_pushedValues, $arguments);
	}
	/*
	 * Invoke es una función de argumentos variables
	 * 
	 * */
	public function invoke () {
		$numArgs = func_num_args();
		$expectedArgs = $this->_expectedArguments();
		if ($numArgs > $expectedArgs) {
			throw new Exts_Exception("Error al invocar closure. Se esperaban $expectedArgs argumentos y se recibieron $numArgs");
		}
		if ($numArgs < $expectedArgs) {
			return $this->_makeNewClosure(func_get_args());
		}
		$arguments = $this->_generateArguments(func_get_args());

		$function = $this->_getFunction();
		

		return $this->_postInvoke(call_user_func_array($function, $arguments));
	}
	protected function _postInvoke ($value){
		return $value;
	}
	public function __toString() {
		return "aClosure";
	}
} 



function example () {
	
	$objetoContexto = new Exts_Closures_Closure("", " echo 'Hola Mundo'; ");
	$closure = new Exts_Closures_Closure('$x', ' $objetoContexto->invoke(); echo "$x\n";  ');
	$closure->objetoContexto=$objetoContexto;
	$closure->invoke("!!");
}












